#region License, Terms and Author(s)
//
// ELMAH - Error Logging Modules and Handlers for ASP.NET
// Copyright (c) 2004-9 Atif Aziz. All rights reserved.
//
//  Author(s):
//
//		Nick Berard, http://www.coderjournal.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

[assembly: Elmah.Scc("$Id: MySqlErrorLog.cs 925 2011-12-23 22:46:09Z azizatif $")]

namespace Elmah
{
    #region Imports

    using System;
    using System.Collections.Generic;
    using System.Data;

    using MySql.Data.MySqlClient;

    using IDictionary = System.Collections.IDictionary;

    #endregion

    /// <summary>
    /// An <see cref="ErrorLog"/> implementation that uses 
    /// <a href="http://www.mysql.com/">MySQL</a> as its backing store.
    /// </summary>

    public class MySqlErrorLog : ErrorLog
    {
        private readonly string _connectionString; 

        /// <summary>
        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class
        /// using a dictionary of configured settings.
        /// </summary>

        public MySqlErrorLog(IDictionary config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            string connectionString = ConnectionStringHelper.GetConnectionString(config);

            //
            // If there is no connection string to use then throw an 
            // exception to abort construction.
            //

            if (connectionString.Length == 0)
                throw new ApplicationException("Connection string is missing for the SQL error log.");

            _connectionString = connectionString;

            //
            // Set the application name as this implementation provides
            // per-application isolation over a single store.
            //

            var appName = config.Find("applicationName", string.Empty);

            if (appName.Length > Commands.MaxAppNameLength)
            {
                throw new ApplicationException(string.Format(
                    "Application name is too long. Maximum length allowed is {0} characters.",
                    Commands.MaxAppNameLength.ToString("N0")));
            }

            ApplicationName = appName;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class
        /// to use a specific connection string for connecting to the database.
        /// </summary>

        public MySqlErrorLog(string connectionString)
        {
            if (connectionString == null)
                throw new ArgumentNullException("connectionString");

            if (connectionString.Length == 0)
                throw new ArgumentException(null, "connectionString");

            _connectionString = connectionString;
        }

        /// <summary>
        /// Gets the name of this error log implementation.
        /// </summary>

        public override string Name
        {
            get { return "MySQL Server Error Log"; }
        }

        /// <summary>
        /// Gets the connection string used by the log to connect to the database.
        /// </summary>

        public virtual string ConnectionString
        {
            get { return _connectionString; }
        }

        /// <summary>
        /// Logs an error to the database.
        /// </summary>
        /// <remarks>
        /// Use the stored procedure called by this implementation to set a
        /// policy on how long errors are kept in the log. The default
        /// implementation stores all errors for an indefinite time.
        /// </remarks>

        public override string Log(Error error)
        {
            if (error == null)
                throw new ArgumentNullException("error");

            string errorXml = ErrorXml.EncodeString(error);
            Guid id = Guid.NewGuid();

            using (MySqlConnection connection = new MySqlConnection(ConnectionString))
            using (MySqlCommand command = Commands.LogError(
                id, ApplicationName,
                error.HostName, error.Type, error.Source, error.Message, error.User,
                error.StatusCode, error.Time.ToUniversalTime(), errorXml))
            {
                command.Connection = connection;
                connection.Open();
                command.ExecuteNonQuery();

                return id.ToString();
            }
        }

        public override bool SupportsSearching
        {
            get { return true; }
        }

        /// <summary>
        /// Returns a page of errors from the databse in descending order 
        /// of logged time.
        /// </summary>
        public override int GetErrors(SearchCriteria criteria, ICollection<ErrorLogEntry> errorEntryList)
        {
            ValidateCriteria(criteria);

            using (MySqlConnection connection = new MySqlConnection(ConnectionString))
            using (MySqlCommand command = GetCommand(ApplicationName, criteria))
            {
                command.Connection = connection;
                connection.Open();

                using (MySqlDataReader reader = command.ExecuteReader())
                {
                    Debug.Assert(reader != null);

                    if (errorEntryList != null)
                    {
                        while (reader.Read())
                        {
                            Error error = new Error();

                            error.ApplicationName = reader["Application"].ToString();
                            error.HostName = reader["Host"].ToString();
                            error.Type = reader["Type"].ToString();
                            error.Source = reader["Source"].ToString();
                            error.Message = reader["Message"].ToString();
                            error.User = reader["User"].ToString();
                            error.StatusCode = Convert.ToInt32(reader["StatusCode"]);
                            error.Time = Convert.ToDateTime(reader.GetString("TimeUtc")).ToLocalTime();

                            errorEntryList.Add(new ErrorLogEntry(this, reader["ErrorId"].ToString(), error));
                        }
                    }
                    reader.Close();
                }

                return (int)command.Parameters["TotalCount"].Value;
            }
        }

        private MySqlCommand GetCommand(string ApplicationName, SearchCriteria criteria)
        {
            if (criteria.ContainsExtendedSearchCriteria)
            {
                return Commands.SearchErrorsXml(ApplicationName, criteria);
            }
            else
            {
                return Commands.GetErrorsXml(ApplicationName, criteria.PageIndex, criteria.PageSize);
            }
        }

        /// <summary>
        /// Returns the specified error from the database, or null 
        /// if it does not exist.
        /// </summary>

        public override ErrorLogEntry GetError(string id)
        {
            if (id == null)
                throw new ArgumentNullException("id");

            if (id.Length == 0)
                throw new ArgumentException(null, "id");

            Guid errorGuid;

            try
            {
                errorGuid = new Guid(id);
            }
            catch (FormatException e)
            {
                throw new ArgumentException(e.Message, "id", e);
            }

            string errorXml = null;

            using (MySqlConnection connection = new MySqlConnection(ConnectionString))
            using (MySqlCommand command = Commands.GetErrorXml(ApplicationName, errorGuid))
            {
                command.Connection = connection;
                connection.Open();

                using (MySqlDataReader reader = command.ExecuteReader())
                {
                    Debug.Assert(reader != null);

                    while (reader.Read())
                    {
                        errorXml = reader.GetString("AllXml");
                    }
                    reader.Close();
                }
            }

            if (errorXml == null)
                return null;

            Error error = ErrorXml.DecodeString(errorXml);
            return new ErrorLogEntry(this, id, error);
        }

        private static class Commands
        {
            public const int MaxAppNameLength = 60;

            /// <summary>
            /// Factory-type class to create parameters of the correct names and sizes.
            /// PD: I presume MySQL is sensitive about not overrunning its parameter sizes,
            /// hence all the substringing.
            /// </summary>
            private static class MakeParam
            {
                private static void SetStringValue(MySqlParameter p, object value)
                {
                    if (value == DBNull.Value || value == null)
                    {
                        p.Value = DBNull.Value;
                    }
                    else
                    {
                        string s = value.ToString();
                        p.Value = s.Substring(0, Math.Min(p.Size, s.Length));
                        p.Value = value;
                    }
                }

                // Beware: the procs have an "ErrorId" and an "Id".
                public static MySqlParameter ErrorId(Guid value)
                {
                    return new MySqlParameter("ErrorId", MySqlDbType.String, 36) { Value = value.ToString() };
                }

                // Beware: the procs have an "ErrorId" and an "Id".
                public static MySqlParameter Id(Guid value)
                {
                    return new MySqlParameter("Id", MySqlDbType.String, 36) { Value = value.ToString() };
                }

                public static MySqlParameter PageIndex(int value)
                {
                    return new MySqlParameter("PageIndex", MySqlDbType.Int32) { Value = value };
                }

                public static MySqlParameter PageSize(int value)
                {
                    return new MySqlParameter("PageSize", MySqlDbType.Int32) { Value = value };
                }

                public static MySqlParameter TotalCount()
                {
                    return new MySqlParameter("TotalCount", MySqlDbType.Int32) { Direction = ParameterDirection.Output };
                }

                // Beware: the procs have an "App" and an "Application".
                public static MySqlParameter Application(string value)
                {
                    var p = new MySqlParameter("Application", MySqlDbType.VarChar, MaxAppNameLength);
                    SetStringValue(p, value);
                    return p;
                }

                // Beware: the procs have an "App" and an "Application".
                public static MySqlParameter App(string value)
                {
                    var p = new MySqlParameter("App", MySqlDbType.VarChar, MaxAppNameLength);
                    SetStringValue(p, value);
                    return p;
                }

                public static MySqlParameter Host(object value)
                {
                    var p = new MySqlParameter("Host", MySqlDbType.VarChar, 50);
                    SetStringValue(p, value);
                    return p;
                }

                public static MySqlParameter Type(object value)
                {
                    var p = new MySqlParameter("Type", MySqlDbType.VarChar, 100);
                    SetStringValue(p, value);
                    return p;
                }

                public static MySqlParameter Source(object value)
                {
                    var p = new MySqlParameter("Source", MySqlDbType.VarChar, 60);
                    SetStringValue(p, value);
                    return p;
                }

                public static MySqlParameter Message(object value)
                {
                    var p = new MySqlParameter("Message", MySqlDbType.VarChar, 500);
                    SetStringValue(p, value);
                    return p;
                }

                public static MySqlParameter User(object value)
                {
                    var p = new MySqlParameter("User", MySqlDbType.VarChar, 50);
                    SetStringValue(p, value);
                    return p;
                }

                public static MySqlParameter StatusCode(int value)
                {
                    return new MySqlParameter("StatusCode", MySqlDbType.Int32) { Value = value };
                }

                public static MySqlParameter StatusCodeString(object value)
                {
                    var p = new MySqlParameter("StatusCodeString", MySqlDbType.VarChar, 100);
                    SetStringValue(p, value);
                    return p;
                }

                public static MySqlParameter TimeUtc(DateTime value)
                {
                    return new MySqlParameter("TimeUtc", MySqlDbType.Datetime) { Value = value };
                }

                public static MySqlParameter TimeUtcFrom(object value)
                {
                    return new MySqlParameter("TimeUtcFrom", MySqlDbType.Datetime) { Value = value };
                }

                public static MySqlParameter TimeUtcTo(object value)
                {
                    return new MySqlParameter("TimeUtcTo", MySqlDbType.Datetime) { Value = value };
                }

                public static MySqlParameter AllXml(object value)
                {
                    return new MySqlParameter("AllXml", MySqlDbType.Text) { Value = value };
                }
            }


            public static MySqlCommand LogError
                (
                Guid id, string appName, string hostName, string typeName, string source,
                string message, string user, int statusCode, DateTime time, string xml
                )
            {
                MySqlCommand cmd = new MySqlCommand("elmah_LogError");
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.Add(MakeParam.ErrorId(id));
                cmd.Parameters.Add(MakeParam.Application(appName));
                cmd.Parameters.Add(MakeParam.Host(hostName));
                cmd.Parameters.Add(MakeParam.Type(typeName));
                cmd.Parameters.Add(MakeParam.Source(source));
                cmd.Parameters.Add(MakeParam.Message(message));
                cmd.Parameters.Add(MakeParam.User(user));
                cmd.Parameters.Add(MakeParam.AllXml(xml));
                cmd.Parameters.Add(MakeParam.StatusCode(statusCode));
                cmd.Parameters.Add(MakeParam.TimeUtc(time));

                return cmd;
            }

            public static MySqlCommand GetErrorXml(string appName, Guid id)
            {
                MySqlCommand cmd = new MySqlCommand("elmah_GetErrorXml");
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.Add(MakeParam.Id(id));
                cmd.Parameters.Add(MakeParam.App(appName));

                return cmd;
            }

            public static MySqlCommand GetErrorsXml(string appName, int pageIndex, int pageSize)
            {
                MySqlCommand cmd = new MySqlCommand("elmah_GetErrorsXml");
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.Add(MakeParam.App(appName));
                cmd.Parameters.Add(MakeParam.PageIndex(pageIndex));
                cmd.Parameters.Add(MakeParam.PageSize(pageSize));
                cmd.Parameters.Add(MakeParam.TotalCount());

                return cmd;
            }

            public static MySqlCommand SearchErrorsXml(string appName, SearchCriteria criteria)
            {
                MySqlCommand cmd = new MySqlCommand("elmah_SearchErrorsXml");
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.Add(MakeParam.App(appName));
                cmd.Parameters.Add(MakeParam.PageIndex(criteria.PageIndex));
                cmd.Parameters.Add(MakeParam.PageSize(criteria.PageSize));
                cmd.Parameters.Add(MakeParam.Host(SqlUtils.GetSearchString(criteria.Host)));
                cmd.Parameters.Add(MakeParam.Type(SqlUtils.GetSearchString(criteria.TypeName)));
                cmd.Parameters.Add(MakeParam.Source(SqlUtils.GetSearchString(criteria.Source)));
                cmd.Parameters.Add(MakeParam.Message(SqlUtils.GetSearchString(criteria.Message)));
                cmd.Parameters.Add(MakeParam.User(SqlUtils.GetSearchString(criteria.User)));
                cmd.Parameters.Add(MakeParam.StatusCodeString(SqlUtils.GetSearchString(criteria.StatusCode)));
                cmd.Parameters.Add(MakeParam.TimeUtcFrom(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcFrom)));
                cmd.Parameters.Add(MakeParam.TimeUtcTo(SqlUtils.GetDateTimeParamValue(criteria.TimeUtcTo)));
                cmd.Parameters.Add(MakeParam.AllXml(SqlUtils.GetSearchString(criteria.AllXml)));
                cmd.Parameters.Add(MakeParam.TotalCount());

                return cmd;
            }

            public static void GetErrorsXmlOutputs(MySqlCommand command, out int totalCount)
            {
                Debug.Assert(command != null);
                totalCount = (int)command.Parameters["TotalCount"].Value;
            }
        }
    }
}
